[XCTF Final 2025]Rewrite it in Rust

说实话深度不是特别高

附件给了一个js,一个html,一个wasm
简单看了下,js就是标准的wasm加载框架代码,python -m http.server先看下这个网页长什么样


看到是一个模拟终端,其他啥都没有,直接去看wasm好了
用ghidra反编译
在作弊函数列表可以看到一系列以obfstr开头的函数,结合上下文可以发现是对解密字符串资源地址然后再通过异或解密字符串

因为ghidra输出的反编译是类c,这里直接把加密抠出来先手动解密出地址,然后再手动解密字符串
部分解密出的字符串如注释所示

可以看到这里是在对用户的输入进行比对,要求参数是su -p,我们随便在终端里试一下

看来是需要密码,查看一下下面的加密流程
这里似乎是创建了一个缓冲区,然后是大量的wgpu调用,看起来是把输入填充到缓冲区后采用gpu计算

gpu上运行的着色器对象是由createShaderModuleAPI创建的,这个API只能在js层,我们直接插入代码打印着色器对象的具体代码,这样就不用从wasm里翻了

可以获取到如下的着色器代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
struct _in_block {
inner: array<u32>,
}

var<private> g_1_1: vec3<u32>;
@group(0) @binding(0)
var<storage> _in: _in_block;
@group(0) @binding(1)
var<storage, read_write> _out: _in_block;

fn qR(s: ptr<function, array<u32, 16>>, a: u32, b: u32, c: u32, d: u32) {
let _e57 = (*s)[a];
let _e59 = (*s)[b];
(*s)[a] = (_e57 + _e59);
let _e63 = (*s)[d];
let _e65 = (*s)[a];
(*s)[d] = (_e63 ^ _e65);
let _e69 = (*s)[d];
let _e73 = (*s)[d];
(*s)[d] = ((_e69 << bitcast<u32>(16u)) | (_e73 >> bitcast<u32>(16u)));
let _e79 = (*s)[c];
let _e81 = (*s)[d];
(*s)[c] = (_e79 + _e81);
let _e85 = (*s)[b];
let _e87 = (*s)[c];
(*s)[b] = (_e85 ^ _e87);
let _e91 = (*s)[b];
let _e95 = (*s)[b];
(*s)[b] = ((_e91 << bitcast<u32>(12u)) | (_e95 >> bitcast<u32>(20u)));
let _e101 = (*s)[a];
let _e103 = (*s)[b];
(*s)[a] = (_e101 + _e103);
let _e107 = (*s)[d];
let _e109 = (*s)[a];
(*s)[d] = (_e107 ^ _e109);
let _e113 = (*s)[d];
let _e117 = (*s)[d];
(*s)[d] = ((_e113 << bitcast<u32>(8u)) | (_e117 >> bitcast<u32>(24u)));
let _e123 = (*s)[c];
let _e125 = (*s)[d];
(*s)[c] = (_e123 + _e125);
let _e129 = (*s)[b];
let _e131 = (*s)[c];
(*s)[b] = (_e129 ^ _e131);
let _e135 = (*s)[b];
let _e139 = (*s)[b];
(*s)[b] = ((_e135 << bitcast<u32>(7u)) | (_e139 >> bitcast<u32>(25u)));
return;
}

fn xY(z: u32) -> array<u32, 16> {
var t: array<u32, 16> = array<u32, 16>();
var i: u32 = u32();
var var_for_index: array<u32, 8> = array<u32, 8>();
var w: array<u32, 16> = array<u32, 16>();
var r: u32 = u32();
var j: u32 = u32();

t[i32()] = 1768978533u;
t[1i] = 1948281188u;
t[2i] = 1701603695u;
t[3i] = 2054299764u;
i = u32();
loop {
let _e67 = i;
if !((_e67 < 8u)) {
break;
}
var_for_index = array<u32, 8>(2778944304u, 2710134585u, 1612543563u, 1172917921u, 3846223579u, 2389091675u, 972033108u, 274738666u);
let _e70 = i;
let _e73 = i;
let _e75 = var_for_index[_e73];
t[(4u + _e70)] = _e75;
continue;
continuing {
let _e76 = i;
i = (_e76 + 1u);
}
}
t[12i] = z;
t[13i] = 888566743u;
t[14i] = 3797923964u;
t[15i] = 2911175433u;
let _e82 = t;
w = _e82;
r = u32();
loop {
let _e83 = r;
if !((_e83 < 10u)) {
break;
}
qR((&w), u32(), 1u, 2u, 3u);
qR((&w), 4u, 5u, 6u, 7u);
qR((&w), 8u, 9u, 10u, 11u);
qR((&w), 12u, 13u, 14u, 15u);
qR((&w), u32(), 4u, 8u, 12u);
qR((&w), 1u, 5u, 9u, 13u);
qR((&w), 2u, 6u, 10u, 14u);
qR((&w), 3u, 7u, 11u, 15u);
qR((&w), u32(), 5u, 10u, 15u);
qR((&w), 1u, 6u, 11u, 12u);
qR((&w), 2u, 7u, 8u, 13u);
qR((&w), 3u, 4u, 9u, 14u);
continue;
continuing {
let _e86 = r;
r = (_e86 + 1u);
}
}
j = u32();
loop {
let _e88 = j;
if !((_e88 < 16u)) {
break;
}
let _e91 = j;
let _e94 = w[_e91];
let _e95 = j;
let _e97 = t[_e95];
w[_e91] = (_e94 + _e97);
continue;
continuing {
let _e99 = j;
j = (_e99 + 1u);
}
}
let _e101 = w;
return _e101;
}

fn m_inner(g: vec3<u32>) {
var k: u32 = u32();
var var_for_index_1_: array<u32, 16> = array<u32, 16>();

let _e56 = (g.x * 16u);
if (_e56 >= arrayLength((&_in.inner))) {
return;
}
let _e60 = xY(g.x);
k = u32();
loop {
let _e61 = k;
if !((_e61 < 16u)) {
break;
}
var_for_index_1_ = _e60;
let _e64 = k;
let _e68 = k;
let _e72 = _in.inner[(_e56 + _e68)];
let _e73 = k;
let _e75 = var_for_index_1_[_e73];
_out.inner[(_e56 + _e64)] = (_e72 ^ _e75);
continue;
continuing {
let _e77 = k;
k = (_e77 + 1u);
}
}
return;
}

fn m_1() {
let _e50 = g_1_1;
m_inner(_e50);
return;
}

@compute @workgroup_size(64, 1, 1)
fn m(@builtin(global_invocation_id) g_1_: vec3<u32>) {
g_1_1 = g_1_;
m_1();
}

根据4*4矩阵的魔数填充和20轮迭代可以推测这是ChaCha20算法,先不看他是不是魔改了,继续往后看

可以看到这里输出了刚刚的错误提示,所以查看isSuper这个标志是在哪里设置的

看到这里有个明显的循环比对结构,查看一下他的参数是哪里来的
有一个是调了AES来的

另一个可以看到是从gpu里读出来的

然后检查一下那个aes,发现是一个解密过程,也就是整体流程是输入用ChaCha20加密后,与AES解密的数据比对
所以我们从提出来数据后先AES解密,在ChaCha20解密即可

贴一下赛时AI搓的解密

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import struct

# AES 参数 - 注意顺序!
# 从反编译代码: const_aes::aes::cbc::<>::decrypt::h314e8f9f5e7eb882(&local_18,0x100278,0x100298);
# 参数顺序: (output, key_addr, iv_addr)
data = [0x0b, 0xdf, 0x65, 0x3c, 0x6e, 0x13, 0xab, 0x6e, 0xb6, 0x3c, 0x0a, 0x7f, 0xf7, 0x04, 0x2d, 0x3a,
0xf0, 0xd4, 0x7f, 0x18, 0xb0, 0xc8, 0x79, 0xcd, 0xf0, 0xdf, 0x95, 0xa1, 0x24, 0x0f, 0xab, 0x42]

# 从代码看: 0x100278是key, 0x100298是IV
# 数据的前16字节应该是IV,后16字节是key
aes_iv = bytes(data[16:])
aes_key = bytes(data[:16])

print(f"AES Key: {aes_key.hex()}")
print(f"AES IV: {aes_iv.hex()}")

# 完整的 AES 加密数据
aes_enc_data = bytes([ 0xb3, 0x9f, 0xc8, 0xe2, 0x35, 0xfa, 0xd2, 0x2c, 0x3a, 0xd1, 0x6f, 0x37, 0xe7, 0xd9, 0x9c, 0x6a, 0x0e, 0xb0, 0x42, 0xc6, 0x7c, 0xe9, 0x11, 0x44, 0xaf, 0x3d, 0x28, 0xda, 0xe1, 0xb5, 0x45, 0x80, 0x5b, 0xeb, 0x4d, 0x32, 0xee, 0x24, 0xc4, 0xd8, 0x0d, 0x2e, 0x61, 0xfd, 0x4a, 0xe1, 0x6d, 0x99, 0x6c, 0xac, 0x6a, 0x15, 0xc2, 0x78, 0x0a, 0x38, 0x00, 0x94, 0x38, 0x59, 0x51, 0xed, 0xc7, 0xa4, 0xed, 0x6f, 0x60, 0xe0, 0xb9, 0xf3, 0x4d, 0xb1, 0x25, 0x37, 0x93, 0x1e, 0xf6, 0x8b, 0x0e, 0x6b, 0x62, 0x42, 0x87, 0x5a, 0x41, 0xae, 0xc5, 0x98, 0x80, 0xb0, 0x1a, 0x85, 0x3d, 0x21, 0xb4, 0x5a, 0x5e, 0xf9, 0x09, 0xfa, 0xd3, 0x32, 0x58, 0x56, 0xf8, 0xed, 0xeb, 0x48, 0x83, 0x7d, 0xc9, 0x22, 0x8f, 0x97, 0x4e, 0xf7, 0x17, 0x25, 0x48, 0x82, 0x15, 0xe2, 0xa0, 0x84, 0x4e, 0x48, 0x05, 0xff, 0x59, 0xe3, 0xf5, 0xcb, 0x0b, 0xdc, 0x18, 0x54, 0x4f, 0xe5, 0xe5, 0x19, 0x1c, 0x2d, 0xb2, 0xf7, 0xa7, 0xee, 0xae, 0x00, 0xb1, 0xb9, 0x54, 0x48, 0x68, 0xe5, 0xed, 0x31, 0x12, 0xf4, 0x8b, 0xd5, 0x6c, 0xe7, 0x3e, 0x26, 0x3c, 0xa1, 0x32, 0x87, 0xf3, 0x19, 0x84, 0x10, 0x6a, 0x00, 0xd5, 0x68, 0x38, 0x01, 0x7a, 0x89, 0xc0, 0x12, 0xbc, 0x19, 0xdf, 0x91, 0x86, 0xdd, 0x15, 0x20, 0xfd, 0xeb, 0xa5, 0xc8, 0xbc, 0x93, 0xf1, 0xbb, 0x96, 0x81, 0x5d, 0xb4, 0xeb, 0x82, 0x5a, 0x5a, 0x5b, 0x72, 0x25, 0x61, 0x8a, 0x6e, 0x3e, 0x89, 0x77, 0x22, 0xa8, 0x7e, 0xb9, 0xcd, 0x01, 0x86, 0x71, 0xeb, 0xf1, 0xa1, 0x4b, 0x4b, 0x81, 0x34, 0x35, 0x9b, 0x69, 0x53, 0xa4, 0xb3, 0x6c, 0xc0, 0x44, 0xc8, 0x9b, 0x8b, 0x89, 0x72, 0xd7, 0x54, 0x03, 0x8d, 0x2c, 0x8e, 0x4c, 0x01, 0x5a, 0x6f, 0x24, 0x3c, 0x3d, 0x13, 0x4c, 0x83, 0x2a, 0x50, 0xac, 0x94, 0x7f, 0xb2, 0x85, 0xb0, 0x9e, 0x8f, 0x54, 0x44, 0x01, 0x1d, 0xcc, 0xd7, 0x52, 0x16, 0xad, 0x7c, 0x30, 0x37, 0x61, 0x2b, 0x91, 0x8b, 0xe8, 0x82, 0x5a, 0x25, 0x15, 0x90, 0xb4, 0xde, 0x2c, 0xec, 0x6e, 0x5c, 0xd3, 0xa9, 0x8f, 0x0c, 0x73, 0x04 ][:256])

print(f"\n=== Step 1: AES-CBC 解密 ===")
print(f"密文长度: {len(aes_enc_data)} bytes")

aes_cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
try:
aes_decrypted = unpad(aes_cipher.decrypt(aes_enc_data), AES.block_size)
print("成功 unpad")
except ValueError as e:
print(f"Unpad 失败: {e}, 使用原始解密结果")
aes_cipher = AES.new(aes_key, AES.MODE_CBC, aes_iv)
aes_decrypted = aes_cipher.decrypt(aes_enc_data)

print(f"AES 解密长度: {len(aes_decrypted)} bytes")
print(f"AES 解密结果 (前64字节): {aes_decrypted[:64].hex()}")

# 检查是否是有效的 uint32 数组
if len(aes_decrypted) % 4 != 0:
print(f"警告: 解密结果长度不是4的倍数!")

print("\n=== Step 2: ChaCha20 解密 ===")

# ChaCha20 常量 (从 shader 代码)
CHACHA_CONSTANTS = [1768978533, 1948281188, 1701603695, 2054299764]

CHACHA_KEY = [
2778944304, 2710134585, 1612543563, 1172917921,
3846223579, 2389091675, 972033108, 274738666
]

CHACHA_NONCE = [888566743, 3797923964, 2911175433]

def rotl(x, n):
"""32位循环左移"""
return ((x << n) & 0xFFFFFFFF) | (x >> (32 - n))

def quarter_round(state, a, b, c, d):
"""ChaCha20 Quarter Round - 完全按照 shader 实现"""
state[a] = (state[a] + state[b]) & 0xFFFFFFFF
state[d] = state[d] ^ state[a]
state[d] = rotl(state[d], 16)

state[c] = (state[c] + state[d]) & 0xFFFFFFFF
state[b] = state[b] ^ state[c]
state[b] = rotl(state[b], 12)

state[a] = (state[a] + state[b]) & 0xFFFFFFFF
state[d] = state[d] ^ state[a]
state[d] = rotl(state[d], 8)

state[c] = (state[c] + state[d]) & 0xFFFFFFFF
state[b] = state[b] ^ state[c]
state[b] = rotl(state[b], 7)

def chacha20_block(counter):
"""生成一个 ChaCha20 块 - 完全按照 shader xY 函数"""
# 初始状态 (shader 中的 t 数组)
state = [0] * 16
state[0:4] = CHACHA_CONSTANTS
state[4:12] = CHACHA_KEY
state[12] = counter
state[13:16] = CHACHA_NONCE

working_state = state.copy()

# 10 个 double round
for _ in range(10):
# qR((&w), u32(), 1u, 2u, 3u);
quarter_round(working_state, 0, 1, 2, 3)
quarter_round(working_state, 4, 5, 6, 7)
quarter_round(working_state, 8, 9, 10, 11)
quarter_round(working_state, 12, 13, 14, 15)

# qR((&w), u32(), 4u, 8u, 12u);
quarter_round(working_state, 0, 4, 8, 12)
quarter_round(working_state, 1, 5, 9, 13)
quarter_round(working_state, 2, 6, 10, 14)
quarter_round(working_state, 3, 7, 11, 15)

# qR((&w), u32(), 5u, 10u, 15u);
quarter_round(working_state, 0, 5, 10, 15)
quarter_round(working_state, 1, 6, 11, 12)
quarter_round(working_state, 2, 7, 8, 13)
quarter_round(working_state, 3, 4, 9, 14)

# w[_e91] = (_e94 + _e97);
for i in range(16):
working_state[i] = (working_state[i] + state[i]) & 0xFFFFFFFF

return working_state

def chacha20_decrypt(ciphertext_bytes):
"""解密 - 按照 shader m_inner 函数的逻辑"""
# 转换为 uint32 数组
num_u32 = len(ciphertext_bytes) // 4
ciphertext_u32 = list(struct.unpack(f'<{num_u32}I', ciphertext_bytes[:num_u32*4]))

plaintext_u32 = []

# 处理每个块 (每块16个uint32 = 64字节)
for block_idx in range((num_u32 + 15) // 16):
offset = block_idx * 16
keystream = chacha20_block(block_idx)

# _out.inner[(_e56 + _e64)] = (_e72 ^ _e75);
for i in range(16):
if offset + i < num_u32:
plaintext_u32.append(ciphertext_u32[offset + i] ^ keystream[i])

return plaintext_u32

# 解密
password_u32 = chacha20_decrypt(aes_decrypted)
print(f"ChaCha20 解密得到 {len(password_u32)} 个 uint32")

# 转换为字节
password_bytes = struct.pack(f'<{len(password_u32)}I', *password_u32)
print(f"解密结果长度: {len(password_bytes)} bytes")
print(f"解密结果 (hex): {password_bytes.hex()}")
print(f"解密结果 (前64字节): {password_bytes[:64]}")

# 多种解码尝试
print(f"\n=== 解码尝试 ===")

# 1. UTF-8
try:
password_str = password_bytes.rstrip(b'\x00').decode('utf-8')
print(f"UTF-8: {password_str}")
except:
print("UTF-8 解码失败")

# 2. ASCII (仅可打印字符)
printable = ''
for b in password_bytes:
if 32 <= b < 127:
printable += chr(b)
elif b == 0:
break
else:
printable += f'\\x{b:02x}'
print(f"可打印字符: {printable}")

# 3. 查找已知字符串模式
if b'flag' in password_bytes.lower():
print("发现 'flag' 字样!")
if b'xctf' in password_bytes.lower():
print("发现 'xctf' 字样!")

# 4. 尝试不同起始位置
print(f"\n不同偏移的可打印字符:")
for offset in [0, 4, 8, 12, 16]:
snippet = password_bytes[offset:offset+32]
readable = ''.join(chr(b) if 32 <= b < 127 else '.' for b in snippet)
print(f"Offset {offset:2d}: {readable}")

反正能解出来,不是很想改了,不知道为什么他这题ASCII都是存在u64里的

Author

SGSG

Posted on

2025-10-27

Updated on

2025-10-27

Licensed under